Пишем продвинутый планировщик с использованием React, Nest и NX. Часть 3: работа с задачами

от автора

Друзья, всем привет! Меня зовут Игорь Карелин, я frontend-разработчик в компании Домклик. В прошлой части мы разобрали, как создать аутентификацию с помощью библиотеки Passport, а сегодня мы рассмотрим такие манипуляции, как добавление, редактирование, удаление и получение задач. Для начала давайте разберём HTTP и некоторые типы запросов.

Коротко о HTTP и типах запросов

HTTP (англ. HyperText Transfer Protocol, «протокол передачи гипертекста») — протокол прикладного уровня передачи данных, изначально — в виде гипертекстовых документов в формате HTML, а в настоящее время используется для передачи произвольных данных.

Метод HTTP — последовательность из любых символов, кроме управляющих и разделителей, указывающая на основную операцию над ресурсом. Для разграничения действий с ресурсами на уровне HTTP-методов и были придуманы следующие варианты:

  • GET — получение ресурса;

  • POST — создание ресурса;

  • PUT — обновление ресурса;

  • DELETE — удаление ресурса.

Это основные методы которые мы будем использовать в нашем приложении. С полным списком и подробнее о HTTP можно почитать по ссылке.

Создание файлов

Давайте запустим Docker и наше приложение, дополнив структуру с помощью команд Nest CLI.

$ nx run-many --parallel --target=serve --projects=backend,frontend // Запускаем приложение $ nest g module tasks // Создаём модуль tasks $ nest g service tasks --no-spec // Создаём сервис без файла тестов $ nest g controller tasks --no-spec // Создаём контроллер без файла тестов

При работе через Nest CLI автоматически импортируются файлы в модули, что очень удобно. Далее руками создадим несколько файлов: task.dto.ts, task.interface.ts и task.model.ts

Создаем mongoose-схему

Теперь поговорим о том, какой минимальный набор данных нам потребуется в задаче, и опишем нашу схему. Чтобы не переусложнять нашу модель, я предлагаю ограничиться заголовком, подробным описанием, датами создания и обновления. Для начала опишем interface. Приступим!

export interface TaskInterface {   title: string;   description: string; }

Тут всё просто, мы пишем, что у нас будет заголовок и описание. Далее создадим модель и схему в файле task.model.ts.

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document } from 'mongoose'; import { TaskInterface } from '../interfaces/task.interface';  @Schema({ collection: 'tasks', timestamps: true }) export class TaskModel extends Document implements TaskInterface {      @Prop({ required: true })   title: string;      @Prop({ required: true })   description: string;    }  export const TaskSchema = SchemaFactory.createForClass(TaskModel); 

В декораторе @Schema мы указываем название коллекции в базе данных и передаём timestamps: true , при создании и обновлении данных в коллекции у нас будут автоматически записываться даты. После создания задачи это будет выглядеть так:

Объект задачи
{     "description": "description",     "title": "title",     "_id": "63a37f9566316dec1d2b131f",     "createdAt": "2022-12-21T21:50:13.569Z",     "updatedAt": "2022-12-21T21:50:13.569Z",     "__v": 0 }

Далее мы создаём класс TaskModel и наследуем от интерфейса TaskInterface, создав при этом обязательные поля с заголовком и описанием. Вызываем SchemaFactory.createForClass, передав в качестве аргумента TaskModel, и экспортируем полученное значение. После этого не забываем добавить в модуль task.module.ts.

Описываем контроллер

В прошлой части мы подробно разобрали, что такое контроллеры и для чего они нужны. Давайте определимся с корневым маршрутом для задачи и зададим ему путь /api/tasks. Нам потребуются следующие методы:

  • POST — создание задачи;

  • PUT — изменение задачи;

  • DELETE — удаление задачи;

  • GET — получение одной или всех задач.

В NestJs методы описываются с помощью декораторов, что очень удобно.

Доступ к манипуляциям с данными есть только у пользователя, который ранее авторизовался и создал задачи под своей учётной записью.

tasks.controller.ts
import {   Body,   Controller,   Delete,   Get,   Param,   Post,   Put,   Req,   UseGuards,   UsePipes,   ValidationPipe, } from '@nestjs/common'; import { TasksService } from './tasks.service'; import { AuthGuard } from '@nestjs/passport'; import { TaskDto } from './dto/task.dto'; import { UsersService } from '../users/users.service';  @Controller('tasks') export class TasksController {   constructor(     private readonly tasksService: TasksService,     private readonly usersService: UsersService   ) {}   @UsePipes(new ValidationPipe())   @UseGuards(AuthGuard('jwt'))   @Post()   async createTask(@Req() req, @Body() tasksDto: TaskDto) {     return await this.tasksService.createTask(req.user._id, tasksDto);   }    @UseGuards(AuthGuard('jwt'))   @Get()   async getAllTasks(@Req() req) {     return await this.tasksService.getAllTasks(req.user.tasks);   }    @UseGuards(AuthGuard('jwt'))   @Get(':id')   async getTask(@Req() req, @Param('id') id) {     return await this.tasksService.getTask(req.user.tasks, id);   }    @UseGuards(AuthGuard('jwt'))   @Delete()   async deleteTask(@Req() req, @Body() body) {     return await this.tasksService.deleteTask(req.user, body.id);   }    @UsePipes(new ValidationPipe())   @UseGuards(AuthGuard('jwt'))   @Put()   async updateTask(@Req() req, @Body() tasksDto: TaskDto) {     return await this.tasksService.updateTask(req.user, tasksDto);   } } 

Итак, мы описали файл контроллера с импортом сервисов, проверкой и методами, перечисленными ранее. Обратите внимание на декораторы @UseGuards(AuthGuard('jwt')) и @Req() req: благодаря ранее созданной аутентификации можно получать данные пользователя при каждом запросе к маршруту. Мы можем проверять доступ к задачам, которые относятся только к текущему пользователю. Подробнее изучить логику выборки конкретных данных, относящихся к пользователю, мы можем в сервисе.

Пишем сервисы

В сервисах у нас написана логика работы с данными и CRUD-операции. Перед тем как взглянуть на сервис задач, давайте посмотрим, какой код добавился и для чего он нужен.

В ранее созданныйUserModel в 17 строку добавили отношение «одни ко многим», чтобы можно было с лёгкостью определить, какие задачи доступны конкретному пользователю.

user.model.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document, Types } from 'mongoose'; import { IUser } from '../interfaces/user.interface'; import { TaskModel } from '../../tasks/models/task.model';  @Schema({ collection: 'users', timestamps: true }) export class UserModel extends Document implements IUser {   @Prop({ required: true })   username: string;    @Prop({ required: true })   password: string;    @Prop({ required: true })   email: string;    @Prop({ type: [Types.ObjectId], ref: TaskModel.name }) // Указываем тип данных и имя модели данных.   tasks: TaskModel[]; }  export const UserSchema = SchemaFactory.createForClass(UserModel);

После этих манипуляций при получении данных пользователя мы увидим ID задач, относящихся к нему. Выглядит это так:

Объект пользователя
{     "_id": "63a37ed666316dec1d2b131b",     "username": "admin",     "email": "example@mail.com",     "tasks": [         "63a37f9566316dec1d2b131f",         "63a39099609d041ff6cd62ab",         "63a3909a609d041ff6cd62af"     ] }

Далее мы дополнили UsersService методами добавления и удаления ID задач из модели пользователя. Методы представлены в 65 и 69 строках.

users.service.ts
import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; import { UserDto } from './dto/user.dto'; import { CreateUserDto } from './dto/user.create.dto'; import { LoginUserDto } from './dto/user-login.dto'; import { toUserDto } from '../shared/mapper'; import { InjectModel } from '@nestjs/mongoose'; import { UserModel } from './models/user.model'; import { Model } from 'mongoose'; import { genSalt, hash, compare } from 'bcrypt';  @Injectable() export class UsersService {   constructor(     @InjectModel(UserModel.name)     private readonly userModel: Model<UserModel>   ) {}    async findOne(options?: object): Promise<UserDto> {     const user = await this.userModel.findOne(options).exec();     return toUserDto(user);   }    async findByLogin({ username, password }: LoginUserDto): Promise<UserDto> {     const user = await this.userModel.findOne({ username }).exec();      if (!user) {       throw new HttpException('User not found', HttpStatus.UNAUTHORIZED);     }      const areEqual = await compare(password, user.password);      if (!areEqual) {       throw new HttpException('Invalid credentials', HttpStatus.UNAUTHORIZED);     }      return toUserDto(user);   }    async findByPayload({ username }: any): Promise<UserDto> {     return await this.findOne({ username });   }    async create(userDto: CreateUserDto): Promise<UserDto> {     const { username, password, email } = userDto;      const userInDb = await this.userModel.findOne({ username }).exec();     if (userInDb) {       throw new HttpException('User already exists', HttpStatus.BAD_REQUEST);     }      const salt = await genSalt(10);     const hashPassword = await hash(password, salt);      const user: UserModel = await new this.userModel({       username,       password: hashPassword,       email,     });      await user.save();      return toUserDto(user);   }    async setTaskToCurrentUser(_id, taskId) {     await this.userModel.updateOne({ _id }, { $push: { tasks: taskId } });   }    async deleteTaskToCurrentUser(_id, taskId) {     await this.userModel.updateOne({ _id }, { $pull: { tasks: taskId } });   } }

Давайте перейдём к сервису задач.

tasks.service.ts
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { TaskDto } from './dto/task.dto'; import { TaskModel } from './models/task.model'; import { UsersService } from '../users/users.service';  @Injectable() export class TasksService {   constructor(     @InjectModel(TaskModel.name)     private readonly taskModel: Model<TaskModel>,     private readonly userService: UsersService   ) {}   async createTask(userId, taskDto: TaskDto) {     const task = await this.taskModel.create(taskDto);     await this.userService.setTaskToCurrentUser(userId, task._id);     return task;   }    async getAllTasks(tasksId) {     return await this.taskModel.find().where('_id').in(tasksId).exec();   }    async getTask(tasks, id: string) {     if (this.taskExistence(tasks, id)) {       return await this.taskModel.findOne({ _id: id }).exec();     }   }    async deleteTask(user, id) {     if (this.taskExistence(user.tasks, id)) {       await this.userService.deleteTaskToCurrentUser(user._id, id);       return await this.taskModel.deleteOne({ _id: id }).exec();     }      throw new HttpException('Task not found', HttpStatus.NOT_FOUND);   }    async updateTask(user, body) {     if (this.taskExistence(user.tasks, body._id)) {       return await this.taskModel.updateOne(body).exec();     }     throw new HttpException('Task not found', HttpStatus.NOT_FOUND);   }    private taskExistence(userTasks, taskId) {     return userTasks.find((id) => taskId === id.toString());   } } 

В сервисе представлены методы добавления, изменения, получения и удаления задач, а также приватный метод для проверки наличия запрашиваемой задачи у пользователя.

Я опишу каждый метод подробнее.

createTask — метод создания задач. При вызове мы получаем ID пользователя, задачу с заголовком и описанием, далее записываем в базу и в ответ получаем объект с ID задачи. Потом вызывается метод setTaskToCurrentUser, в который передаётся ID пользователя и ID задачи, а затем ищется пользователь по ID и запись ID задачи найденному пользователю.

getAllTasks — тут всё просто: передаётся список ID задач текущего пользователя и дальнейшее их получение.

getTask — получаем список задач текущего пользователя и запрашиваемую задачу, далее проверяем, есть ли она у пользователя. Если есть — возвращаем задачу, если нет — вернём пустой ответ.

deleteTask — как и в getTask, получаем список задач текущего пользователя и запрашиваемую задачу, далее проверяем, есть ли она у пользователя. Если есть — удаляем ID задачи из модели пользователя и удаляем саму задачу, иначе возвращаем ошибку.

updateTask — проверяем наличие задачи у пользователя. Если она есть, изменяем её на новую, иначе возвращаем ошибку, что задача не найдена.

taskExistence — последний представленный приватный метод, который проверяет, есть ли текущий ID задачи у пользователя.

Добавляем данные в модуль

На заключительном этапе нам необходимо проверить, что все ранее созданные данные добавлены в модуль.

tasks.module.ts
import { Module } from '@nestjs/common'; import { TasksService } from './tasks.service'; import { TasksController } from './tasks.controller'; import { MongooseModule } from '@nestjs/mongoose'; import { TaskModel, TaskSchema } from './models/task.model'; import { UsersModule } from '../users/users.module';  @Module({   imports: [     UsersModule,     MongooseModule.forFeature([       {         name: TaskModel.name,         schema: TaskSchema,       },     ]),   ],   providers: [TasksService],   controllers: [TasksController], }) export class TasksModule {} 

В этом файле мы импортировали UsersModule для использования UsersService в TasksService. Добавили сервис, контроллер и модель задач для работы с базой данных. Не забываем проверить, что TasksModule присутствует в AppModule.

На этом пока остановимся. Мы поговорили о HTTP и разработали методы работы с задачами. В следующей, финальной части мы напишем весь фронтенд. Спасибо за внимание!

Исходный код доступен по ссылке.


ссылка на оригинал статьи https://habr.com/ru/company/domclick/blog/706964/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *