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