Как стать автором
Обновить

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

Время на прочтение8 мин
Количество просмотров4.5K

Друзья, всем привет! Меня зовут Игорь Карелин, я 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 и разработали методы работы с задачами. В следующей, финальной части мы напишем весь фронтенд. Спасибо за внимание!

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

Теги:
Хабы:
Всего голосов 14: ↑14 и ↓0+14
Комментарии0

Публикации

Информация

Сайт
domclick.ru
Дата регистрации
Дата основания
Численность
501–1 000 человек
Местоположение
Россия
Представитель
Dangorche