From 026d656a3c459a97f76864d253342095cc0ff2a6 Mon Sep 17 00:00:00 2001 From: Andrey Demidkin Date: Thu, 12 Dec 2024 14:21:48 +0300 Subject: [PATCH 1/2] add solution for 2-module 2-task --- 02-nestjs-basics/02-filtering/package.json | 4 ++ .../02-filtering/tasks/task.model.ts | 4 ++ .../02-filtering/tasks/tasks.controller.ts | 30 +++++++++- .../02-filtering/tasks/tasks.service.ts | 58 ++++++++++++++++++- package.json | 4 +- 5 files changed, 93 insertions(+), 7 deletions(-) diff --git a/02-nestjs-basics/02-filtering/package.json b/02-nestjs-basics/02-filtering/package.json index 1bb8bbe..eb53405 100644 --- a/02-nestjs-basics/02-filtering/package.json +++ b/02-nestjs-basics/02-filtering/package.json @@ -2,5 +2,9 @@ "scripts": { "start": "nest start --watch", "test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --no-warnings --experimental-vm-modules\" NODE_ENV=test jest --config jest.config.js --runInBand" + }, + "dependencies": { + "@types/lodash": "^4.17.13", + "lodash": "^4.17.21" } } diff --git a/02-nestjs-basics/02-filtering/tasks/task.model.ts b/02-nestjs-basics/02-filtering/tasks/task.model.ts index 270faa9..67d857b 100644 --- a/02-nestjs-basics/02-filtering/tasks/task.model.ts +++ b/02-nestjs-basics/02-filtering/tasks/task.model.ts @@ -10,3 +10,7 @@ export interface Task { description: string; status: TaskStatus; } + +export type TaskKeys = keyof Task + +export const TASK_KEYS:TaskKeys[] = ['id', 'status', 'description', 'status']; diff --git a/02-nestjs-basics/02-filtering/tasks/tasks.controller.ts b/02-nestjs-basics/02-filtering/tasks/tasks.controller.ts index 088f979..3caef98 100644 --- a/02-nestjs-basics/02-filtering/tasks/tasks.controller.ts +++ b/02-nestjs-basics/02-filtering/tasks/tasks.controller.ts @@ -1,6 +1,6 @@ -import { Controller, Get, Query } from "@nestjs/common"; +import {Controller, Get, HttpException, HttpStatus, Query} from "@nestjs/common"; import { TasksService } from "./tasks.service"; -import { TaskStatus } from "./task.model"; +import {TASK_KEYS, TaskKeys, TaskStatus} from "./task.model"; @Controller("tasks") export class TasksController { @@ -11,5 +11,29 @@ export class TasksController { @Query("status") status?: TaskStatus, @Query("page") page?: number, @Query("limit") limit?: number, - ) {} + @Query("sortBy") sortBy?: TaskKeys, + ) { + const wrongSortParameter = sortBy && !TASK_KEYS.includes(sortBy); + const wrongPageParameter = page && typeof Number(page) !== "number" + || page && page < 1 + const wrongLimitParameter = limit && typeof Number(limit) !== "number" + || limit && limit < 1 + const wrongStatusParameter = status && !Object.values(TaskStatus).includes(status); + + if(wrongLimitParameter) { + throw new HttpException(`Wrong limit parameter`, HttpStatus.BAD_REQUEST); + } + if(wrongPageParameter ) { + throw new HttpException(`Wrong page parameter`, HttpStatus.BAD_REQUEST); + } + if(wrongSortParameter) { + throw new HttpException(`Wrong sortBy parameter`, HttpStatus.BAD_REQUEST); + } + + if(wrongStatusParameter) { + throw new HttpException(`Wrong task status:${status}`, HttpStatus.NOT_FOUND); + } + + return this.tasksService.getFilteredTasks(status, page, limit, sortBy); + } } diff --git a/02-nestjs-basics/02-filtering/tasks/tasks.service.ts b/02-nestjs-basics/02-filtering/tasks/tasks.service.ts index a7b04c4..cc9d3fe 100644 --- a/02-nestjs-basics/02-filtering/tasks/tasks.service.ts +++ b/02-nestjs-basics/02-filtering/tasks/tasks.service.ts @@ -1,5 +1,23 @@ -import { Injectable } from "@nestjs/common"; -import { Task, TaskStatus } from "./task.model"; +import {Injectable} from "@nestjs/common"; +import {Task, TaskKeys, TaskStatus} from "./task.model"; +import {chunk} from "lodash"; + +//метод сортировки задач по полям +const sortByTaskField = (sortBy: TaskKeys, tasks:Task[]) => tasks.sort((taskA, taskB)=> { + //если поле сортировки "id" то сравниваем как числа + let valueA: number | string = sortBy === 'id' ? Number(taskA[sortBy]) : taskA[sortBy]; + let valueB: number | string = sortBy === 'id' ? Number(taskB[sortBy]) : taskB[sortBy]; + + if(valueA < valueB) { + return -1; + } + + if(valueA > valueB) { + return 1; + } + + return 0; +}) @Injectable() export class TasksService { @@ -40,5 +58,39 @@ export class TasksService { status?: TaskStatus, page?: number, limit?: number, - ): Task[] {} + sortBy?: TaskKeys, + ): Task[] { + let result = this.tasks; + //сначала фильтруем задачи по статусу + if(status) { + result = result.filter(task => task.status === status); + } + + //сортируем по полю, если нужно + if(sortBy) { + result = sortByTaskField(sortBy, result); + } + + //если есть limit + if(limit) { + //разбиваем одномерный массив задач. + //получаем двумерный. Кол-во элементов во вложенном = limit + const tasksByPage = chunk(result, limit); + + //если есть page и он больше 1, + // то обращаемся к массиву задач по индексу page - 1. + //если элемента с таким индексом нет, то возвращаем пустой массив + //если же page нет, то возвращаем первый элемент + return (page) + ? tasksByPage[page - 1] ?? [] + : tasksByPage[0] + } + + //если нет лимита и page больше 1,то возвращаем пустой массив + if(page && page > 1) { + return [] + } + + return result + } } diff --git a/package.json b/package.json index 6290711..f43f399 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,9 @@ "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "sqlite3": "^5.1.7", - "typeorm": "^0.3.20" + "typeorm": "^0.3.20", + "@types/lodash": "^4.17.13", + "lodash": "^4.17.21" }, "devDependencies": { "@nestjs/cli": "^10.0.0", From e7f794c9d266636f59b6549cb0eb45eafa1f0c16 Mon Sep 17 00:00:00 2001 From: Andrey Demidkin Date: Mon, 6 Jan 2025 16:38:37 +0300 Subject: [PATCH 2/2] add solution for 4-module 1-task --- .../01-nestjs-components/errors.log | 0 .../filters/http-error.filter.ts | 20 +++++++++++++++++-- .../guards/roles.guard.ts | 13 ++++++++++-- .../interceptors/api-version.interceptor.ts | 15 ++++++++++++-- .../01-nestjs-components/main.ts | 2 ++ .../middlewares/logging.middleware.ts | 8 ++++++-- .../pipes/parse-int.pipe.ts | 12 +++++++++-- .../tasks/tasks.controller.ts | 17 ++++++++++++---- .../tasks/tasks.module.ts | 9 +++++++-- .../tasks/tasks.service.ts | 2 +- 10 files changed, 81 insertions(+), 17 deletions(-) create mode 100644 04-request-lifecycle/01-nestjs-components/errors.log diff --git a/04-request-lifecycle/01-nestjs-components/errors.log b/04-request-lifecycle/01-nestjs-components/errors.log new file mode 100644 index 0000000..e69de29 diff --git a/04-request-lifecycle/01-nestjs-components/filters/http-error.filter.ts b/04-request-lifecycle/01-nestjs-components/filters/http-error.filter.ts index f879724..22fa77f 100644 --- a/04-request-lifecycle/01-nestjs-components/filters/http-error.filter.ts +++ b/04-request-lifecycle/01-nestjs-components/filters/http-error.filter.ts @@ -1,5 +1,21 @@ -import { ArgumentsHost, ExceptionFilter } from "@nestjs/common"; +import {ArgumentsHost, Catch, ExceptionFilter, HttpException} from "@nestjs/common"; +import { Response } from 'express'; +import fs from 'node:fs'; +@Catch(HttpException) export class HttpErrorFilter implements ExceptionFilter { - catch(exception: any, host: ArgumentsHost) {} + catch(exception: HttpException, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const status = exception.getStatus(); + const dateString = new Date().toISOString(); + const logMessage = `[${dateString}] ${status} - ${exception.message}\n`; + fs.appendFileSync('errors.log', logMessage); + + response.status(status).json({ + statusCode: status, + message: exception.message, + timestamp: dateString, + }); + } } diff --git a/04-request-lifecycle/01-nestjs-components/guards/roles.guard.ts b/04-request-lifecycle/01-nestjs-components/guards/roles.guard.ts index 470c45c..61bd950 100644 --- a/04-request-lifecycle/01-nestjs-components/guards/roles.guard.ts +++ b/04-request-lifecycle/01-nestjs-components/guards/roles.guard.ts @@ -1,5 +1,14 @@ -import { CanActivate, ExecutionContext } from "@nestjs/common"; +import {CanActivate, ExecutionContext, ForbiddenException, Injectable} from "@nestjs/common"; +@Injectable() export class RolesGuard implements CanActivate { - canActivate(context: ExecutionContext) {} + canActivate(context: ExecutionContext) { + const request = context.switchToHttp().getRequest(); + const userRole = request.headers["x-role"]; + if(userRole !== "admin") { + throw new ForbiddenException('Доступ запрещён: требуется роль admin') + } + + return true; + } } diff --git a/04-request-lifecycle/01-nestjs-components/interceptors/api-version.interceptor.ts b/04-request-lifecycle/01-nestjs-components/interceptors/api-version.interceptor.ts index ee49ab8..af7f1f2 100644 --- a/04-request-lifecycle/01-nestjs-components/interceptors/api-version.interceptor.ts +++ b/04-request-lifecycle/01-nestjs-components/interceptors/api-version.interceptor.ts @@ -1,5 +1,16 @@ -import { NestInterceptor, ExecutionContext, CallHandler } from "@nestjs/common"; +import {NestInterceptor, ExecutionContext, CallHandler, Injectable} from "@nestjs/common"; +import { map } from "rxjs/operators"; +@Injectable() export class ApiVersionInterceptor implements NestInterceptor { - intercept(context: ExecutionContext, next: CallHandler) {} + intercept(context: ExecutionContext, next: CallHandler) { + const startTime = new Date().getTime(); + return next.handle().pipe( + map((data) => ({ + ...data, + apiVersion: "1.0", + executionTime: (new Date().getTime()) - startTime + 'ms' + })), + ); + } } diff --git a/04-request-lifecycle/01-nestjs-components/main.ts b/04-request-lifecycle/01-nestjs-components/main.ts index 72a150e..56c4d0b 100644 --- a/04-request-lifecycle/01-nestjs-components/main.ts +++ b/04-request-lifecycle/01-nestjs-components/main.ts @@ -1,8 +1,10 @@ import { NestFactory } from "@nestjs/core"; import { AppModule } from "./app.module"; +import {HttpErrorFilter} from "./filters/http-error.filter"; async function bootstrap() { const app = await NestFactory.create(AppModule); + app.useGlobalFilters(new HttpErrorFilter()); await app.listen(process.env.PORT ?? 3000); } bootstrap(); diff --git a/04-request-lifecycle/01-nestjs-components/middlewares/logging.middleware.ts b/04-request-lifecycle/01-nestjs-components/middlewares/logging.middleware.ts index 028f62a..d1d3c1b 100644 --- a/04-request-lifecycle/01-nestjs-components/middlewares/logging.middleware.ts +++ b/04-request-lifecycle/01-nestjs-components/middlewares/logging.middleware.ts @@ -1,6 +1,10 @@ -import { NestMiddleware } from "@nestjs/common"; +import {Injectable, NestMiddleware} from "@nestjs/common"; import { Request, Response, NextFunction } from "express"; +@Injectable() export class LoggingMiddleware implements NestMiddleware { - use(req: Request, res: Response, next: NextFunction) {} + use(req: Request, res: Response, next: NextFunction) { + console.log(`[${req.method}] ${req.url}`); + next(); // Передаём управление следующему обработчику + } } diff --git a/04-request-lifecycle/01-nestjs-components/pipes/parse-int.pipe.ts b/04-request-lifecycle/01-nestjs-components/pipes/parse-int.pipe.ts index cd68a51..b27d203 100644 --- a/04-request-lifecycle/01-nestjs-components/pipes/parse-int.pipe.ts +++ b/04-request-lifecycle/01-nestjs-components/pipes/parse-int.pipe.ts @@ -1,5 +1,13 @@ -import { PipeTransform } from "@nestjs/common"; +import {BadRequestException, Injectable, PipeTransform} from "@nestjs/common"; +@Injectable() export class ParseIntPipe implements PipeTransform { - transform(value: string): number {} + transform(value: string): number { + const result = +value; + if(isNaN(result)){ + throw new BadRequestException(`"${value}" не является числом`); + } + + return result; + } } diff --git a/04-request-lifecycle/01-nestjs-components/tasks/tasks.controller.ts b/04-request-lifecycle/01-nestjs-components/tasks/tasks.controller.ts index 5d0a494..11a8875 100644 --- a/04-request-lifecycle/01-nestjs-components/tasks/tasks.controller.ts +++ b/04-request-lifecycle/01-nestjs-components/tasks/tasks.controller.ts @@ -4,33 +4,40 @@ import { Delete, Get, Param, - ParseIntPipe, Patch, - Post, + Post, UseGuards, UseInterceptors, } from "@nestjs/common"; import { TasksService } from "./tasks.service"; import { CreateTaskDto, UpdateTaskDto } from "./task.model"; +import {ParseIntPipe} from "../pipes/parse-int.pipe"; +import {RolesGuard} from "../guards/roles.guard"; +import {ApiVersionInterceptor} from "../interceptors/api-version.interceptor"; @Controller("tasks") export class TasksController { constructor(private readonly tasksService: TasksService) {} @Get() - getAllTasks() { + @UseInterceptors(ApiVersionInterceptor) + async getAllTasks() { return this.tasksService.getAllTasks(); } @Get(":id") - getTaskById(@Param("id", ParseIntPipe) id: number) { + @UseInterceptors(ApiVersionInterceptor) + getTaskById(@Param("id", new ParseIntPipe()) id: number) { return this.tasksService.getTaskById(id); } @Post() + @UseGuards(RolesGuard) + @UseInterceptors(ApiVersionInterceptor) createTask(@Body() task: CreateTaskDto) { return this.tasksService.createTask(task); } @Patch(":id") + @UseInterceptors(ApiVersionInterceptor) updateTask( @Param("id", ParseIntPipe) id: number, @Body() task: UpdateTaskDto, @@ -39,6 +46,8 @@ export class TasksController { } @Delete(":id") + @UseGuards(RolesGuard) + @UseInterceptors(ApiVersionInterceptor) deleteTask(@Param("id", ParseIntPipe) id: number) { return this.tasksService.deleteTask(id); } diff --git a/04-request-lifecycle/01-nestjs-components/tasks/tasks.module.ts b/04-request-lifecycle/01-nestjs-components/tasks/tasks.module.ts index adecf42..52459bc 100644 --- a/04-request-lifecycle/01-nestjs-components/tasks/tasks.module.ts +++ b/04-request-lifecycle/01-nestjs-components/tasks/tasks.module.ts @@ -1,10 +1,15 @@ -import { Module } from "@nestjs/common"; +import {MiddlewareConsumer, Module, NestModule} from "@nestjs/common"; import { TasksController } from "./tasks.controller"; import { TasksService } from "./tasks.service"; +import {LoggingMiddleware} from "../middlewares/logging.middleware"; @Module({ imports: [], controllers: [TasksController], providers: [TasksService], }) -export class TasksModule {} +export class TasksModule implements NestModule{ + configure(consumer: MiddlewareConsumer) { + consumer.apply(LoggingMiddleware).forRoutes("/"); + } +} diff --git a/04-request-lifecycle/01-nestjs-components/tasks/tasks.service.ts b/04-request-lifecycle/01-nestjs-components/tasks/tasks.service.ts index 4e08463..b746862 100644 --- a/04-request-lifecycle/01-nestjs-components/tasks/tasks.service.ts +++ b/04-request-lifecycle/01-nestjs-components/tasks/tasks.service.ts @@ -5,7 +5,7 @@ import { CreateTaskDto, Task, TaskStatus, UpdateTaskDto } from "./task.model"; export class TasksService { private tasks: Task[] = []; - getAllTasks() { + async getAllTasks() { return { tasks: this.tasks }; }