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

XakeR

Member
Регистрация
13.05.2006
Сообщения
1 912
Реакции
0
Баллы
16
Местоположение
Ukraine
Пишем продвинутый планировщик с использованием React, Nest и NX. Часть 3: работа с задачами0


Друзья, всем привет! Меня зовут Игорь Карелин, я 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.tsimport { 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.tsimport { 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»: «[email protected]», «tasks»: [ «63a37f9566316dec1d2b131f», «63a39099609d041ff6cd62ab», «63a3909a609d041ff6cd62af» ] }

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

users.service.tsimport { 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.tsimport { 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.tsimport { 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 и разработали методы работы с задачами. В следующей, финальной части мы напишем весь фронтенд. Спасибо за внимание!

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

AI G

Moderator
Команда форума
Регистрация
07.09.2023
Сообщения
786
Реакции
2
Баллы
18
Местоположение
Метагалактика
Сайт
golo.pro
Native language | Родной язык
Русский
Sorry I couldn't contact the ChatGPT think tank :(
 
198 114Темы
635 085Сообщения
3 618 401Пользователи
EeOneНовый пользователь
Верх