Доброго дня, хабровчане!
Как и обещал, в продолжение своего пет-проекта по созданию грид-компонента опишу здесь создание backend части на таком фреймворке как NestJS, попутно ознакомив читателя с дополнительными инструментами для backend разработки. Код проекта найдете здесь. Статья в основном для новичков, поэтому не пытайтесь найти здесь что-то сверхъестественное.
Сразу сделаю оговорку, что я не являюсь крутым специалистом по данному фреймворку, скорее – большим его любителем. Но почему все-таки NestJS, а не какой-нибудь FastApi (Python), Spring Boot (Java) или еще какой-нибудь модный backend фреймворк, которым также пользуется прогрессивная общественность? Все просто — котики!
Обзор котиков от NestJS
![Котик на главной странице Котик на главной странице](https://habrastorage.org/getpro/habr/upload_files/d55/3ac/8cd/d553ac8cd53275da9f29a9d66cd0072c.png)
![Этого можно увидеть почти в любом разделе Этого можно увидеть почти в любом разделе](https://habrastorage.org/getpro/habr/upload_files/566/2b1/d73/5662b1d733a509a07a70bbe9a218ee2c.png)
![Controllers Controllers](https://habrastorage.org/getpro/habr/upload_files/678/4bf/3c7/6784bf3c7e1f95566f7a42ac4fa50db9.png)
![Официальные курсы по NestJS Официальные курсы по NestJS](https://habrastorage.org/getpro/habr/upload_files/1d4/e5f/a8d/1d4e5fa8d0053c94a2176e3ab76736dd.png)
![Уже не помню где Уже не помню где](https://habrastorage.org/getpro/habr/upload_files/128/b77/618/128b776185753814dfe03868e02d4bf6.png)
![Котик на стуле Котик на стуле](https://habrastorage.org/getpro/habr/upload_files/552/a3c/ada/552a3cadac7ff0cf7b23c9fbdca73dda.png)
P.S. можете еще поискать, возможно их там больше.
Найдете - кидайте в комменты.
Шучу конечно, хотя коты в документации очень забавные.
Первая причина заключается в том, что на основной работе приходится периодически работать с данным фреймворком, копаться в чужом говно коде, дебажить микросервисы и т.п. Ну и во-вторых — фреймворк обрел уже достаточно большую популярность, имеет хорошую документацию (кому-то было не лень перевести ее на русский) и для многих является де-факто стандартом качества разработки RESTfull веб-сервисов. Неплохая обзорная статья по фреймворку находится здесь.
Ну а теперь к делу!
Писать будем также под Linux (Ubuntu). Необходимо иметь установленным NodeJS, npm (откуда и как установить описал в первой статье) ну и как обычно — желание творить!
Содержание
Установка NestJS и создание проекта
Для тех, кто ни разу не работал с фреймворком — опишу как его установить. Так же как и для VueJS, для NestJS существует своя CLI, которая сильно упрощает и ускоряет процесс разработки. Согласно инструкций в официальной документации выполним ее установку:
Установка Nest CLI
Откроем терминал и выполним командуnpm i -g @nestjs/cli
![Установка Nest CLI и проверка версии Установка Nest CLI и проверка версии](https://habrastorage.org/getpro/habr/upload_files/093/2cd/fc6/0932cdfc6854beeee4803e7dfb38daca.png)
на момент написания статьи у меня была версия 9.0.0. Вывод в терминале у Вас наверняка будет отличаться, т.к. у меня уже был установлен nest и данной командой я его просто обновил.
Перейдем в директорию, в которой будем создавать проект и выполним команду по созданию нового проекта nest new grid-component-backend
:
Создание проекта и запуск приложения
Среда nest cli запросит выбрать пакетный менеджер. Я выберу npm (и Вам тоже советую) и жмем Enter:
![Выбор npm в качестве менеджера пакетов Выбор npm в качестве менеджера пакетов](https://habrastorage.org/getpro/habr/upload_files/3c0/e11/9d0/3c0e119d0facd961e3a6e3aad5b3b53d.png)
ждем окончания процесса создания проекта:
![Прогресс создания проекта Прогресс создания проекта](https://habrastorage.org/getpro/habr/upload_files/aa1/9b0/055/aa19b0055ffe55e67f14892500841475.png)
После успешного создания проекта в терминале можно увидеть команды для запуска приложения:
![Команды для запуска приложения и ссылочка на донат =) Команды для запуска приложения и ссылочка на донат =)](https://habrastorage.org/getpro/habr/upload_files/eba/dd9/b33/ebadd9b33eae12364aef390021063ee5.png)
P.S. можете задонатить и порадовать разработчиков, но я, пожалуй, пропущу этот момент =)
Давайте проверим — все ли хорошо. Перейдем в папку с проектомcd grid-component-backend
и стартанем наше приложение — npm run start
:
![Запуск приложения Запуск приложения](https://habrastorage.org/getpro/habr/upload_files/99e/b0c/c64/99eb0cc64f8afa55cd17e0811d933d34.png)
Зелененькие логи говорят о том, что все прошло успешно и по адресу http://127.0.0.1:3000 будет доступно наше приложение:
![Hello World на NestJS Hello World на NestJS](https://habrastorage.org/getpro/habr/upload_files/aa1/831/31f/aa183131f6fdb8e923b0fa399e21c9ce.png)
Можно останавливать запущенный сервер (Ctrl+C) и закрывать терминал. Далее будем работать с проектом в VS Code.
В качестве СУБД для нашего небольшого приложения будем использовать легковесную sqlite.
Настройка Hot Reload
Nest также предоставляет возможность каждый раз не перезапускать/билдить проект при внесении изменений. Если Вы как и я используете Nest CLI, то для настройки этой возможности достаточно выполнить следующие действия:
Установить необходимые пакеты с помощью команды
npm i --save-dev webpack-node-externals run-script-webpack-plugin webpack
В корне проекта создать файл webpack-hmr.config.js и наполнить его следующим содержимым
В файл src/main.ts добавить вот такие данные
В файле package.json подкорректировать команду для запуска проекта
Сохраняем все изменения, останавливаем проект. В дальнейшем для запуска проекта будем использовать команду npm run start:dev
.
В целом...эта штука работает, но почему-то иногда выскакивают вот такие ошибки:
Вывод в консоли при запуске в режиме Hot Reload
![Ошибки при изменении данных Ошибки при изменении данных](https://habrastorage.org/getpro/habr/upload_files/f83/e0a/74a/f83e0a74a3b33b170c411ac4675be052.png)
видимо не все работает гладко, поэтому webpack иногда просит перезапустить приложение.
Для автоматического перезапуска приложения можно установить свойство autoRestart в значение true в файле webpack-hmr.config.js:
![Свойство autoRestart Свойство autoRestart](https://habrastorage.org/getpro/habr/upload_files/e46/67b/6da/e4667b6da3320274b4ecf67e0619c470.png)
Если кто сталкивался с такой проблемой и знает с чем это связано и как это вылечить — пишите в комментариях.
Настраиваем проект для работы с sqlite
Добавим необходимые зависимости для работы с sqlite. Как сказано в документации — Nest из коробки предоставляет пакет @nestjs/typeorm для использования TypeORM в качестве работы с базами данных. Установим этот пакет, а также зависимости для работы с СУБД sqlite c помощью команды npm install --save @nestjs/typeorm typeorm sqlite3
. В файле package.json можно увидеть эти изменения.
В файл главного модуля добавим настройки подключения к нашей БД. В случае sqlite их будет не очень много:
app.module.ts
В итоге файл корневого модуля будет выглядеть следующим образом:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DocumentsModule } from './documents/documents.module';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: './test_db.sqlite',
synchronize: true,
autoLoadEntities: true,
}),
DocumentsModule // А это уже импорт нашего модуля для работы с доками,
// о нем говорится в следующем разделе
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule { }
В раздел импортов мы добавили модуль (класс) TypeOrmModule, вызвав его метод forRoot() с необходимыми параметрами. Судя по типу возвращаемого значения (см. скриншот):
![Тип возвращаемого значения метода forRoot() Тип возвращаемого значения метода forRoot()](https://habrastorage.org/getpro/habr/upload_files/24f/26b/652/24f26b6524e6457a4193944e45e8f50d.png)
этот метод возвращает некий динамический модуль, который поможет нам подключиться к БД.
В сам же метод forRoot() мы передали тип используемой СУБД (sqlite) и путь к файлу test_db.sqlite (этот файл БД будет создан автоматически во время очередной сборки приложения), а также свойства synchronize и autoLoadEntities. Подробнее об этих свойствах можно почитать здесь.
Создание модуля для работы с документами
Приложения на NestJS имеют модульную архитектуру. Подробней можно почитать в уже приведенной мной статье (она небольшая, советую Вам ознакомиться с ней).
Каждый новый модуль по-хорошему должен храниться в отдельной папке с одноименным названием и должен быть зарегистрирован в корневом модуле приложения (файл app.module.ts). Можно все это сделать вручную, но мы воспользуемся Nest CLI, которая сделает все это за нас. Перейдем в папку с проектом и выполним команду nest g module documents:
![Результат создания модуля documents Результат создания модуля documents](https://habrastorage.org/getpro/habr/upload_files/84b/e43/1e9/84be431e9f296e478e1deda76302de4d.png)
Как видно из рисунка — эта команда сгенерировала папку documents в папке src, добавила туда файл documents.module.ts и зарегистрировала этот модуль в файле app.module.ts. Вот эти изменения.
Далее, создадим папку src/documents/entities для хранения сущностей модуля Document (хоть сущность у нас будет всего одна), а в этой папке — файл document.entity.ts со следующим содержимым:
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Document {
@PrimaryGeneratedColumn()
id: Number;
@Column({ length: 100, unique: true })
cipher: String;
@Column()
createdOn: Date;
@Column()
inArchive: Boolean;
}
Не буду подробно описывать все декораторы, я думаю их названия говорят сами за себя. В любом случае Вам так или иначе придется курить доки по TypeORM для проектирования своих баз данных =).
Зарегистрируем эту сущность в модуле document:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Document } from './entities/document.entity';
@Module({
imports: [TypeOrmModule.forFeature([Document])],
})
export class DocumentsModule {}
Если hot reload работает нормально, то при сохранении и сборке проекта в корневом каталоге будет создан файл базы данных test_db.sqlite.
Убедимся, что в этой БД создана таблица document с необходимыми атрибутами
Подключаемся к БД
Для этого нам нужно установить утилиту sqlite3. Инструкцию по установке можно почитать здесь.
Перейдем в терминале в папку с проектом и выполним команду sqlite3 test_db.sqlite
![sqlite3 command line shell sqlite3 command line shell](https://habrastorage.org/getpro/habr/upload_files/602/5d8/2de/6025d82decdd4bcde1f086ec5cf42fda.png)
Выполним команду .database
и убедимся что мы подключились к нашей БД:
![Вывод списка БД Вывод списка БД](https://habrastorage.org/getpro/habr/upload_files/006/1d2/39c/0061d239c973c4b1e3332b3de5369292.png)
Команда .fullschema
выведет нам созданную таблицу (DDL) с ее свойствами:
![Вывод команды .fullschema Вывод команды .fullschema](https://habrastorage.org/getpro/habr/upload_files/36b/bff/fcf/36bbfffcf657564c1c25bcea980225b6.png)
Так что пока все идет по плану.
Добавим в таблицу запись и выведем содержимое:
![Добавление записи в таблицу и вывод данных Добавление записи в таблицу и вывод данных](https://habrastorage.org/getpro/habr/upload_files/717/415/5f6/7174155f6f6f8b1fa7c19510b96c6743.png)
Создание контроллера и сервиса
Еще одна важная составляющая любого RESTfull сервиса — это контроллеры. Если кратко, в NestJS контроллеры нужны для хранения эндпоинтов. Простыми словами, эндпоинт — это адрес (URI), по которому Вы будете обращаться в адресной строке браузера (либо другого web-клиента, например, Postman, о котором мы еще поговорим), чтобы получить доступ к данным приложения, — например, получить все документы из БД, добавить новый документ, обновить существующий, вывести документы по определенному фильтру, удалить один или несколько документов и т.д. Вместе с адресом иногда (я бы сказал чаще всего!) необходимо передавать дополнительные параметры: заголовки, тело запроса, параметры запроса, тип HTTP-запроса и др. Совокупность всех этих эндпоинтов, их грамотное описание, — что и для чего нужно вызывать с описанием всех параметров HTTP-запроса/ответа представляет собой т.н. API-интерфейс Вашего приложения.
Мне очень нравится определение из википедии:
Если программу (модуль, библиотеку) рассматривать как чёрный ящик, то API — это набор «ручек», которые доступны пользователю данного ящика и которые он может вертеть и дёргать.
Чтобы создать контроллер — выполним команду nest g controller documents
:
![Результат создания контроллера Результат создания контроллера](https://habrastorage.org/getpro/habr/upload_files/9a9/2e5/ed9/9a92e5ed92f952687809c7f9d6dbbda3.png)
Мы получили на выходе файлы documents.controller.ts (сам файл контроллера) и файл для тестирования documents.controller.spec.ts, который нам не понадобится. CLI также зарегистрировала контроллер в модуле для документов и поместила все в папку с соответствующим модулем. Вот коммит этих изменений.
Однако контроллеры необходимы лишь для хранения/описания эндпоинтов. Непосредственно для реализации всей логики нам нужен еще один важный элемент Nest приложения — провайдер (сервис).
Для создания сервиса выполним, как Вы могли уже догадаться nest g service documents
. Результат будет абсолютно аналогичен предыдущему, ссылка на коммит.
Data Transfer Object (DTO)
Перед тем как приступить к реализации контроллера, — создадим в папке src/documents папку dto и добавим в нее два файла:
Файл create-document.dto.ts с таким содержимым:
export class CreateDocumentDto {
id: Number;
cipher: String;
createdOn: Date;
inArchive: Boolean;
}
и paging-document.dto.ts с вот таким:
import { CreateDocumentDto } from './create-document.dto';
export class PagingDocumentDto extends CreateDocumentDto {
paging: {};
sorting: {};
filtering: {};
}
Подробнее про DTO можно почитать тут. Простыми словами, DTO — это вспомогательные объекты (классы), с помощью которых мы будем взаимодействовать с нашими сущностями, которые мы создали ранее в папке entities, т.е. что-то вроде трансфера между сущностями в БД и телом HTTP - запросов (в оф. документации описание DTO как раз и лежит в разделе Request payloads). Также там говорится что лучше определять DTO-объекты как классы, что мы и сделали в вышеприведенных файлах.
В create-document.dto.ts у нас находится класс для работы с документами — как Вы заметили в нем абсолютно такие же свойства, как и в сущности "документ". Класс PagingDocumentDto в файле paging-document.dto.ts наследует CreateDocumentDto и добавляет еще три свойства для пагинации, сортировки и фильтрации. Зачем они нам нужны — узнаем уже совсем скоро.
Реализация API. Dependency injection
Добавим в контроллер вот такую строчку (импортировав при этом DocumentService):
constructor(private readonly documentService: DocumentService) {}
Казалось бы...какой-то конструктор. Но данной строчкой мы внедрили в наш контроллер сервис, в котором будет находиться реализация всех методов для работы с документами и тем самым применили на практике такой подход как Dependency injection. Фреймворк NestJS (и не только он) целиком и полностью построен на этом паттерне. А чтобы мы могли использовать в нашем контроллере функционал DocumentService, — он (сервис), в свою очередь, должен быть снабжен декоратором @Injectable(), который говорит о том, что сервис можно внедрять. Это уже является реализацией такого принципа как IoS, об этом тоже упоминается в документации по Nest.
Ну все, хватит умных слов....поехали дальше!
Для начала создадим два метода (роута), — GET, на который мы будем "стучаться" чтобы получить все документы из БД:
@Get()
findAll() {
return this.documentService.findAllDocuments();
}
и POST для создания документа:
@Post()
create(@Body() createDocumentDto: CreateDocumentDto) {
return this.documentService
.createDocument(createDocumentDto)
.then((response) => {
return response;
})
.catch((error) => {
throw new HttpException('Произошла какая-то ошибка при создании документа =(: ' + error, HttpStatus.INTERNAL_SERVER_ERROR);
})
}
На данном этапе файл documents.controller.ts должен быть таким:
documents.controller.ts
import { DocumentsService } from './documents.service';
import { CreateDocumentDto } from './dto/create-document.dto';
import {
Body,
Controller,
Get,
HttpException,
HttpStatus,
Post
} from '@nestjs/common';
@Controller('documents')
export class DocumentsController {
constructor(private readonly documentService: DocumentsService) { }
@Get()
findAll() {
return this.documentService.findAllDocuments();
}
@Post()
create(@Body() createDocumentDto: CreateDocumentDto) {
return this.documentService
.createDocument(createDocumentDto)
.then((response) => {
return response;
})
.catch((error) => {
throw new HttpException('Произошла какая-то ошибка при создании документа =(: ' + error, HttpStatus.INTERNAL_SERVER_ERROR);
})
}
}
Декоратор@Bodyговорит о том, что параметр createDocumentDto будет передан через тело POST-запроса. В нашем случае тело запроса будет в формате JSON, ключи которого будут совпадать со свойствами класса CreateDocumentDto
Теперь нам осталось реализовать методы findAllDocuments() и createDocument() для получения и создания документов, соответственно.
Вот как будет выглядеть метод для получения документов:
async findAllDocuments(): Promise<Document[]> {
return await this.dataSource.getRepository(Document).find();
}
А вот так для создания:
async createDocument(createDocumentDto: CreateDocumentDto): Promise<InsertResult> {
return await this.dataSource
.createQueryBuilder()
.insert()
.into(Document)
.values([
{
cipher: createDocumentDto.cipher,
createdOn: createDocumentDto.createdOn,
inArchive: createDocumentDto.inArchive,
},
])
.execute();
}
В обоих случаях мы воспользовались преимуществом async/await синтаксиса. В последних версиях TypeORM для доступа к данным необходимо обращаться к объекту DataSource для взаимодействия с БД.
Ну что, пробуем "дергать" наше API...!
Чтобы выполнить GET-запрос для получения документов достаточно открыть любой браузер и в адресную строку вставить 127.0.0.1:3000/documents. Вот, примерно, что должно получиться:
![Ответ на запрос по получению документов Ответ на запрос по получению документов](https://habrastorage.org/getpro/habr/upload_files/459/b11/539/459b115395aa0bf2c609e78771686cbd.png)
Оказывается Mozilla умеет красиво выводить JSON ответы. Это означает, что все прошло успешно и мы получили ВСЕ документы из базы данных, который у нас всего один.
Ссылка на коммит вышеперечисленных изменений.
Postman. Тестируем API и заполняем БД
Выполнить GET-запрос из предыдущего раздела в браузере не составило особого труда, но как я уже говорил выше, существуют другие типы HTTP запросов, в которые нужно передавать дополнительные параметры — тело, различные заголовки, вложения и т.д.
Наверное, самой распространенной программкой для выполнения HTTP-запросов является Postman. О том как его установить — почитаете в официальной документации. А о его возможностях есть несколько статей на хабре, например тут и тут.
Давайте выполним POST-запрос и добавим новый документ в БД. Вот как это выглядит:
![POST-запрос для добавления записи в БД POST-запрос для добавления записи в БД](https://habrastorage.org/getpro/habr/upload_files/f19/2bf/d99/f192bfd994f7e8062d6bc2a9f19bf383.png)
cURL запроса
curl --location --request POST '127.0.0.1:3000/documents'
--header 'Content-Type: application/json'
--data-raw '{
"cipher": "7e0535f9-666e-45c4-843d-6c366a4a9d80",
"createdOn": "2019-03-18T15:49:00.000Z",
"inArchive": false
}'
Имея подобный cURL — очень легко создать запрос, просто импортировав его в Postman. В нем уже содержатся все необходимые параметры для выполнения запроса. Делается это следующим образом:
-
Нажимаем "Import":
Импорт -
Переходим на вкладку "Raw text", вставляем cURL и нажимаем "Continue":
Добавляем cURL -
На следующей вкладке нажимаем "Import":
И еще раз Import -
Бьютифицируем наш JSON:
Делаем JSON красивый Наш запрос готов. Осталось нажать кнопку "Send".
-
А чтобы создать cURL из имеющегося запроса — нужно нажать в правом-верхнем углу кнопку со знаком "</>", выбрать в выпадалке cURL и копировать готовый текст:
Копируем запрос как cURL
После отправки запроса — в ответе должен прийти id созданной записи с 201-ым кодом ответа, который говорит о том, что все прошло успешно и запись была создана:
![Результат выполнения запроса по добавлению документа Результат выполнения запроса по добавлению документа](https://habrastorage.org/getpro/habr/upload_files/ed0/2b3/50b/ed02b350b4f88d5b4b5d5bb0fdfb8750.png)
Но что, если нам нужно добавить сотню/тысячу записей. Причем желательно, чтобы данные не дублировались. Можно, конечно, написать небольшой скрипт на каком-либо языке программирования с использованием библиотеки для работы с REST, но, раз уж мы начали изучать Postman, сделаем это с его помощью.
Массовое добавление записей в Postman
Для начала сохраним наш POST-запрос в какой-нибудь папке:
![Сохраним запрос в отдельной папке Сохраним запрос в отдельной папке](https://habrastorage.org/getpro/habr/upload_files/1e3/84e/55e/1e384e55e880196f134bbc328ce645e0.png)
затем изменим тело запроса следующим образом:
![Использование переменных окружения Использование переменных окружения](https://habrastorage.org/getpro/habr/upload_files/5ce/610/bf0/5ce610bf0ebb9407df2b274d54db27aa.png)
в двойных фигурных скобках мы используем т.н. переменные окружения.
Далее перейдем на вкладку "Pre-request Script" и добавим следующий JS код:
![Код для генерации тела запроса с рандомными данными Код для генерации тела запроса с рандомными данными](https://habrastorage.org/getpro/habr/upload_files/ff7/344/c19/ff7344c19db1fe295c7e2da6a92427f7.png)
Данная вкладка предназначена для того, чтобы добавить JavaScript код, который будет выполнен ПЕРЕД выполнением самого запроса. Тем самым мы имеем возможность задавать переменные окружения в фигурных скобках, определенные нами выше в теле запроса.
Сначала мы определили функцию randomDate(start, end), которая возвращает рандомную дату в заданном интервале (самому было думать лень, поэтому взял код отсюда).
Чтобы назначить переменной значение, необходимо воспользоваться следующим синтаксисом, который нам предоставляет Postman:
pm.environment.set("variable_key", "variable_value");
Задаем переменную createdOn, передав в функцию randomDate начало и конец интервала:
pm.environment.set("createdOn", randomDate(new Date(2012, 0, 1), new Date()));
Генерировать рандомное булевое значение inArchive (а почему бы и нет!) можно вот так:
pm.environment.set("inArchive", Math.random() < 0.5);
Получить уникальный GUID для поля cipher можно следующим образом:
pm.environment.set("cipher", "{{$guid}}");
обязательно сохраняем все изменения:
![Сохраняем изменения Сохраняем изменения](https://habrastorage.org/getpro/habr/upload_files/682/917/dcb/682917dcb077f4f5533620833d33ec77.png)
кликаем правой кнопкой мыши по папке с запросом и выбираем пункт "Run collection":
![](https://habrastorage.org/getpro/habr/upload_files/a50/cbe/d7f/a50cbed7f8403baa4027bf15f654e2c7.png)
В открывшемся окне у нас имеется единственный POST-запрос, который мы добавили в папку. Т.к. у нас в БД уже есть 2 записи, зададим 98 итераций и поставим галочку напротив "Save responces" чтобы сохранять ответы (мало ли, вдруг что-то пойдет не так):
![Параметры для запуска коллекции Параметры для запуска коллекции](https://habrastorage.org/getpro/habr/upload_files/38f/46a/bc6/38f46abc6787ab3f0a32f2bb74ac212e.png)
Нажимаем "Run grid-component-backend"
Если все пройдет успешно, — Postman выполнит 98 запросов по созданию записей:
![Результат выполнения 98 запросов Результат выполнения 98 запросов](https://habrastorage.org/getpro/habr/upload_files/ae2/5e9/cee/ae25e9ceec9aa9fe8708f6b539bdc5f0.png)
Проверить наличие записей в БД можно тремя способами: напрямую выполнить SQL-запрос в БД, выполнить GET-запрос в браузере как в предыдущем разделе, либо создать в постмэне GET-запрос, который должен вернуть нам JSON-массив с добавленными документами.
Получение cURL из браузера
Покажу как можно получить готовый cURL из вкладки Network браузера для последующего импорта в Postman.
-
Запустим наш браузер (у меня Mozilla, но то же самое прокатит и для Google Chrome) нажмем клавишу F12 чтобы открыть консоль. Введем как и ранее в адресную строку http://127.0.0.1:3000/documents для выполнения GET-запроса и нажмем Enter. На вкладке Network должен появиться наш запрос:
-
Если кликнуть по нему правой кнопкой — в контекстном меню можно найти замечательную опцию Copy as cURL, которая сгенерирует вам cURL и сразу добавит в буфер обмена для последующего импорта в Postman:
Данная опция очень полезна для отладки API в готовых проектах.
Пагинация, сортировка, фильтрация
Так как записей теперь у нас в БД достаточно (целых 100, а могло быть еще больше!) нужно выводить их небольшими порциями. Вообще, пагинация — это отдельная, очень обширная тема, на которую написано немало статей. Погуглив немного по этой теме Вы увидите, что для реализации пагинации применяются в основном два стиля (синтаксиса) — skip-take и limit-offset. Но! это всего лишь два названия одной и той же сущности. Помимо этого каждая СУБД предоставляет свои штатные инструменты для постраничного просмотра данных. Вот ссылка на неплохую статью про пагинацию в Postgres, а вот официальная дока по этой теме.
Первым делом нужно понять, как лучше всего осуществить пагинацию, используя имеющуюся ORM, в нашем случае TypeORM. Давайте же спросим это у гугла, который в первой же ссылке отведет нас на стэковерфлоу. Ну все...осталось только сделать.
Добавим в контроллер следующий метод (по умолчанию POST запрос в NestJS возвращает 201 код ответа, который говорит о создании некоторой записи/объекта, но в данном случае нам не нужно ничего создавать, — нужно просто вернуть записи, поэтому добавим декоратор @HttpCodeс соответствующим кодом ответа):
@Post('/findPaginated')
@HttpCode(HttpStatus.OK)
findPaginated(@Body() pagingDocumentDto: PagingDocumentDto) {
return this.documentService.findPaginated(pagingDocumentDto);
}
а в сервис, соответственно, реализацию метода:
Реализация пагинации
async findPaginated(pagingDocumentDto: PagingDocumentDto) {
let options = {
skip: pagingDocumentDto.paging['skip'],
take: pagingDocumentDto.paging['take'],
order: pagingDocumentDto.sorting,
};
const filter = pagingDocumentDto.filtering;
let filters = {};
// Цикл для динамического формирования фильтров
for (var column in filter) {
if (Object.prototype.hasOwnProperty.call(filter, column)) {
const valueToFilter = filter[column]['valueToFilter'];
const comparisonOperator = filter[column]['comparisonOperator'];
const columnType = filter[column]['columnType'];
if (valueToFilter || valueToFilter === false) {
filters[column] = this.operatorMapping(
columnType,
comparisonOperator,
valueToFilter,
);
}
}
}
options['where'] = filters;
const [documents, count] = await this.dataSource.getRepository(Document).findAndCount(
options,
);
return {
documents,
count,
};
}
operatorMapping(
columnType: string,
comparisonOperator: string,
valueToFilter: any,
): FindOperator<any> {
switch (comparisonOperator) {
case 'likeOperator':
if (['number', 'datetime-local'].includes(columnType))
return Raw(
(alias) =>
`lower(cast(${alias} as text)) like '%${(
valueToFilter + ''
).toLowerCase()}%'`,
);
// if (['string'].includes(columnType))
return Raw(
(alias) =>
`lower(${alias}) like '%${(valueToFilter + '').toLowerCase()}%'`,
);
case 'notLikeOperator':
if (['number', 'datetime-local'].includes(columnType))
return Raw(
(alias) =>
`lower(cast(${alias} as text)) not like '%${(
valueToFilter + ''
).toLowerCase()}%'`,
);
// if (['string'].includes(columnType))
return Raw(
(alias) =>
`lower(${alias}) not like '%${(
valueToFilter + ''
).toLowerCase()}%'`,
);
case 'greaterThanOperator':
return MoreThan(valueToFilter);
case 'greaterThanOrEqualOperator':
return MoreThanOrEqual(valueToFilter);
case 'lessThanOperator':
return LessThan(valueToFilter);
case 'lessThanOrEqualOperator':
return LessThanOrEqual(valueToFilter);
case 'notEqualOperator':
return Not(valueToFilter);
default:
// equalOperator
return Equal(valueToFilter);
}
}
Немного комментариев.
С фронта к нам будет приходить тело запроса, содержащее необходимые параметры. Вот пример такого тела:
{
"paging": {
"skip": 0,
"take": "5"
},
"sorting": {
"createdOn": "DESC"
},
"filtering": {
"createdOn": {
"columnType": "datetime-local",
"comparisonOperator": "greaterThanOrEqualOperator",
"valueToFilter": "2016-02-11T14:44"
},
"cipher": {
"columnType": "text",
"comparisonOperator": "likeOperator",
"valueToFilter": "14"
}
}
}
В объект options сохраняем данные для пагинации (skip, take) и сортировки (order):
let options = {
skip: pagingDocumentDto.paging['skip'],
take: pagingDocumentDto.paging['take'],
order: pagingDocumentDto.sorting,
};
в filter сэйвим данные для фильтрации:
const filter = pagingDocumentDto.filtering;
В объект filters будем динамически добавлять параметры для поиска данных в зависимости от фильтруемых столбцов и операторов сравнения:
let filters = {};
// Цикл для динамического формирования фильтров
for (var column in filter) {
if (Object.prototype.hasOwnProperty.call(filter, column)) {
const valueToFilter = filter[column]['valueToFilter'];
const comparisonOperator = filter[column]['comparisonOperator'];
const columnType = filter[column]['columnType'];
if (valueToFilter || valueToFilter === false) {
filters[column] = this.operatorMapping(
columnType,
comparisonOperator,
valueToFilter,
);
}
}
}
В данном цикле мы перебираем все свойства объекта filtering из тела запроса выше и передаем их в метод operatorMapping(), который формирует нам выражение для поиска.
Для like-поиска был использован метод Raw. Причем для поиска по столбцам с не текстовыми значениями пришлось кастануть их в текстовый тип, ну и привести к одному регистру:
return Raw(
(alias) =>
`lower(cast(${alias} as text)) like '%${(
valueToFilter + ''
).toLowerCase()}%'`,
);
В конце передаем все параметры в метод findAndCount, который возвращает нам найденные записи и их количество:
const [documents, count] = await this.dataSource.getRepository(Document).findAndCount(
options,
);
Запускаем фронт и бэк. Что-то пошло не так. CORS
Итак, все готово — вода, кипящее масло бэк и фронт. Давайте запустим все это вместе.
Как Вы уже успели заметить — линукс установлен у меня на виртуалке. Это, честно говоря, боль! Поэтому чтобы сильно не нагружать систему — я закрою VS Code и запущу фронт и бэк в терминале.
Переходим в папку с бэкендом и запускаем его:
![Запуск backend приложения Запуск backend приложения](https://habrastorage.org/getpro/habr/upload_files/bf7/0ae/e8f/bf70aee8f2552837b9311c7cffed5c74.png)
а теперь фронт (в отдельном терминале):
![Запуск frontend приложения Запуск frontend приложения](https://habrastorage.org/getpro/habr/upload_files/0f8/e1a/e5d/0f8e1ae5d7b6b5a2efa4ce48cb76fd84.png)
Если фронт вы делали по инструкции из первой части, то после запуска должен запуститься браузер, который отобразит грид и попытается загрузить данные. Если успеть нажать F12 и открыть консоль, то можно увидеть вот такое сообщение:
![](https://habrastorage.org/getpro/habr/upload_files/f7c/205/429/f7c2054290012a9fde4c4947fe659d53.png)
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://127.0.0.1:3000/documents/findPaginated. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing). Status code: 404.
Попробую объяснить простым языком. Фронт крутится у нас по адресу 127.0.0.1:8080, а бэк на 127.0.0.1:3000. Т.е. используются разные источники (Origins), поэтому браузер пытается добраться до бэкенда через (cross) фронтенд:
![Cross-Origin Resource Sharing (CORS) Cross-Origin Resource Sharing (CORS)](https://habrastorage.org/getpro/habr/upload_files/6be/b7a/94c/6beb7a94c520fc03681011deedd24de0.png)
но браузеры придерживаются политики одного источника — Same-origin policy (а у нас они разные, т.к. разные порты), поэтому, если не предпринять дополнительные меры, то браузер не позволит выполнить данный запрос на бэкенд.
В NestJS есть отдельная страница по CORS, на которой описано как можно доработать приложение, чтобы браузер не ругался. Есть несколько способов, — я воспользуюсь вот таким (в файл main.ts добавим следующую опцию):
![Доработка приложения для выполнения cross origin requests Доработка приложения для выполнения cross origin requests](https://habrastorage.org/getpro/habr/upload_files/bfb/a30/38f/bfba3038f7e0ed38b250e7c662462bf7.png)
Сделайте такую же доработку, запустите проект с помощью команды npm run start:dev
и обновите страницу в браузере:
![Данные загружены Данные загружены](https://habrastorage.org/getpro/habr/upload_files/206/043/33a/20604333a2b83daf0bc676182e775178.png)
Отлично! Данные загружены, все работает!
P.S> Мозилла почему-то отображает элементы для пагинации слева (а по задумке должны быть справа) и галочки выровнены по центру, хотя предусматривалось, что они будут слева. Хром же отображает все как и я хотел (как в песочнице). Если есть мысли по данному поведению — оставляйте в комментариях.
Заключение
Ну вот, наконец-то...я это сделал! Однако, чтобы Вы понимали, — все что описано в этих двух статьях — это даже не надводная часть, а лишь снежная шапка, покрывающая вершину айсберга под названием Web-разработка. Наверное, где-то вот здесь:
![Красивый айсберг Красивый айсберг](https://habrastorage.org/getpro/habr/upload_files/59f/915/211/59f9152113eac803e4e1208cadf8203b.png)
Тем не менее, очень надеюсь, что материал этих двух статей был хоть сколько-нибудь полезным и послужит отправной точкой для изучения Web-технологий.