GridComponent. Часть II. Котики, NestJS и чуть-чуть о Postman

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

Доброго дня, хабровчане!

Как и обещал, в продолжение своего пет-проекта по созданию грид-компонента опишу здесь создание backend части на таком фреймворке как NestJS, попутно ознакомив читателя с дополнительными инструментами для backend разработки. Код проекта найдете здесь. Статья в основном для новичков, поэтому не пытайтесь найти здесь что-то сверхъестественное.

Сразу сделаю оговорку, что я не являюсь крутым специалистом по данному фреймворку, скорее – большим его любителем. Но почему все-таки NestJS, а не какой-нибудь FastApi (Python), Spring Boot (Java) или еще какой-нибудь модный backend фреймворк, которым также пользуется прогрессивная общественность? Все просто — котики!

Обзор котиков от NestJS
Котик на главной странице
Котик на главной странице
Этого можно увидеть почти в любом разделе
Этого можно увидеть почти в любом разделе
Controllers
Controllers
Официальные курсы по NestJS
Официальные курсы по NestJS
Уже не помню где
Уже не помню где
Котик на стуле
Котик на стуле

P.S. можете еще поискать, возможно их там больше.
Найдете - кидайте в комменты.

Шучу конечно, хотя коты в документации очень забавные.

Первая причина заключается в том, что на основной работе приходится периодически работать с данным фреймворком, копаться в чужом говно коде, дебажить микросервисы и т.п. Ну и во-вторых — фреймворк обрел уже достаточно большую популярность, имеет хорошую документацию (кому-то было не лень перевести ее на русский) и для многих является де-факто стандартом качества разработки RESTfull веб-сервисов. Неплохая обзорная статья по фреймворку находится здесь.

Ну а теперь к делу!

Писать будем также под Linux (Ubuntu). Необходимо иметь установленным NodeJS, npm (откуда и как установить описал в первой статье) ну и как обычно — желание творить!

Содержание

  • Установка NestJS и создание проекта

  • Настройка Hot Reload

  • Настраиваем проект для работы с sqlite

  • Создание модуля для работы с документами

  • Создание контроллера и сервиса

  • Data Transfer Object (DTO)

  • Реализация API. Dependency injection

  • Postman. Тестируем API и заполняем БД

  • Пагинация, сортировка, фильтрация

  • Запускаем фронт и бэк. Что-то пошло не так. CORS

  • Заключение

Установка NestJS и создание проекта

Для тех, кто ни разу не работал с фреймворком — опишу как его установить. Так же как и для VueJS, для NestJS существует своя CLI, которая сильно упрощает и ускоряет процесс разработки. Согласно инструкций в официальной документации выполним ее установку:

Установка Nest CLI

Откроем терминал и выполним командуnpm i -g @nestjs/cli

Установка Nest CLI и проверка версии
Установка Nest CLI и проверка версии

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

Перейдем в директорию, в которой будем создавать проект и выполним команду по созданию нового проекта nest new grid-component-backend:

Создание проекта и запуск приложения

Среда nest cli запросит выбрать пакетный менеджер. Я выберу npm (и Вам тоже советую) и жмем Enter:

Выбор npm в качестве менеджера пакетов
Выбор npm в качестве менеджера пакетов

ждем окончания процесса создания проекта:

Прогресс создания проекта
Прогресс создания проекта

После успешного создания проекта в терминале можно увидеть команды для запуска приложения:

Команды для запуска приложения и ссылочка на донат =)
Команды для запуска приложения и ссылочка на донат =)

P.S. можете задонатить и порадовать разработчиков, но я, пожалуй, пропущу этот момент =)

Давайте проверим — все ли хорошо. Перейдем в папку с проектомcd grid-component-backend и стартанем наше приложение — npm run start:

Запуск приложения
Запуск приложения

Зелененькие логи говорят о том, что все прошло успешно и по адресу http://127.0.0.1:3000 будет доступно наше приложение:

Hello World на NestJS
Hello World на NestJS

Можно останавливать запущенный сервер (Ctrl+C) и закрывать терминал. Далее будем работать с проектом в VS Code.

В качестве СУБД для нашего небольшого приложения будем использовать легковесную sqlite.

Настройка Hot Reload

Nest также предоставляет возможность каждый раз не перезапускать/билдить проект при внесении изменений. Если Вы как и я используете Nest CLI, то для настройки этой возможности достаточно выполнить следующие действия:

  1. Установить необходимые пакеты с помощью команды npm i --save-dev webpack-node-externals run-script-webpack-plugin webpack

  2. В корне проекта создать файл webpack-hmr.config.js и наполнить его следующим содержимым

  3. В файл src/main.ts добавить вот такие данные

  4. В файле package.json подкорректировать команду для запуска проекта

Сохраняем все изменения, останавливаем проект. В дальнейшем для запуска проекта будем использовать команду npm run start:dev .

В целом...эта штука работает, но почему-то иногда выскакивают вот такие ошибки:

Вывод в консоли при запуске в режиме Hot Reload
Ошибки при изменении данных
Ошибки при изменении данных

видимо не все работает гладко, поэтому webpack иногда просит перезапустить приложение.

Для автоматического перезапуска приложения можно установить свойство autoRestart в значение true в файле webpack-hmr.config.js:

Свойство autoRestart
Свойство autoRestart

Если кто сталкивался с такой проблемой и знает с чем это связано и как это вылечить — пишите в комментариях.

Настраиваем проект для работы с 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()

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

В сам же метод forRoot() мы передали тип используемой СУБД (sqlite) и путь к файлу test_db.sqlite (этот файл БД будет создан автоматически во время очередной сборки приложения), а также свойства synchronize и autoLoadEntities. Подробнее об этих свойствах можно почитать здесь.

Создание модуля для работы с документами

Приложения на NestJS имеют модульную архитектуру. Подробней можно почитать в уже приведенной мной статье (она небольшая, советую Вам ознакомиться с ней).

Каждый новый модуль по-хорошему должен храниться в отдельной папке с одноименным названием и должен быть зарегистрирован в корневом модуле приложения (файл app.module.ts). Можно все это сделать вручную, но мы воспользуемся Nest CLI, которая сделает все это за нас. Перейдем в папку с проектом и выполним команду nest g module documents:

Результат создания модуля documents
Результат создания модуля documents

Как видно из рисунка — эта команда сгенерировала папку 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

Выполним команду .database и убедимся что мы подключились к нашей БД:

Вывод списка БД
Вывод списка БД

Команда .fullschema выведет нам созданную таблицу (DDL) с ее свойствами:

Вывод команды .fullschema
Вывод команды .fullschema

Так что пока все идет по плану.

Добавим в таблицу запись и выведем содержимое:

Добавление записи в таблицу и вывод данных
Добавление записи в таблицу и вывод данных

Создание контроллера и сервиса

Еще одна важная составляющая любого RESTfull сервиса — это контроллеры. Если кратко, в NestJS контроллеры нужны для хранения эндпоинтов. Простыми словами, эндпоинт — это адрес (URI), по которому Вы будете обращаться в адресной строке браузера (либо другого web-клиента, например, Postman, о котором мы еще поговорим), чтобы получить доступ к данным приложения, — например, получить все документы из БД, добавить новый документ, обновить существующий, вывести документы по определенному фильтру, удалить один или несколько документов и т.д. Вместе с адресом иногда (я бы сказал чаще всего!) необходимо передавать дополнительные параметры: заголовки, тело запроса, параметры запроса, тип HTTP-запроса и др. Совокупность всех этих эндпоинтов, их грамотное описание, — что и для чего нужно вызывать с описанием всех параметров HTTP-запроса/ответа представляет собой т.н. API-интерфейс Вашего приложения.

Мне очень нравится определение из википедии:

Если программу (модуль, библиотеку) рассматривать как чёрный ящик, то API — это набор «ручек», которые доступны пользователю данного ящика и которые он может вертеть и дёргать.

Чтобы создать контроллер — выполним команду nest g controller documents:

Результат создания контроллера
Результат создания контроллера

Мы получили на выходе файлы 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. Вот, примерно, что должно получиться:

Ответ на запрос по получению документов
Ответ на запрос по получению документов

Оказывается Mozilla умеет красиво выводить JSON ответы. Это означает, что все прошло успешно и мы получили ВСЕ документы из базы данных, который у нас всего один.

Ссылка на коммит вышеперечисленных изменений.

Postman. Тестируем API и заполняем БД

Выполнить GET-запрос из предыдущего раздела в браузере не составило особого труда, но как я уже говорил выше, существуют другие типы HTTP запросов, в которые нужно передавать дополнительные параметры — тело, различные заголовки, вложения и т.д.

Наверное, самой распространенной программкой для выполнения HTTP-запросов является Postman. О том как его установить — почитаете в официальной документации. А о его возможностях есть несколько статей на хабре, например тут и тут.

Давайте выполним POST-запрос и добавим новый документ в БД. Вот как это выглядит:

POST-запрос для добавления записи в БД
POST-запрос для добавления записи в БД
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. В нем уже содержатся все необходимые параметры для выполнения запроса. Делается это следующим образом:

  1. Нажимаем "Import":

    Импорт
    Импорт
  2. Переходим на вкладку "Raw text", вставляем cURL и нажимаем "Continue":

    Добавляем cURL
    Добавляем cURL
  3. На следующей вкладке нажимаем "Import":

    И еще раз Import
    И еще раз Import
  4. Бьютифицируем наш JSON:

    Делаем JSON красивый
    Делаем JSON красивый
  5. Наш запрос готов. Осталось нажать кнопку "Send".

  6. А чтобы создать cURL из имеющегося запроса — нужно нажать в правом-верхнем углу кнопку со знаком "</>", выбрать в выпадалке cURL и копировать готовый текст:

    Копируем запрос как cURL
    Копируем запрос как cURL

После отправки запроса — в ответе должен прийти id созданной записи с 201-ым кодом ответа, который говорит о том, что все прошло успешно и запись была создана:

Результат выполнения запроса по добавлению документа
Результат выполнения запроса по добавлению документа

Но что, если нам нужно добавить сотню/тысячу записей. Причем желательно, чтобы данные не дублировались. Можно, конечно, написать небольшой скрипт на каком-либо языке программирования с использованием библиотеки для работы с REST, но, раз уж мы начали изучать Postman, сделаем это с его помощью.

Массовое добавление записей в Postman

Для начала сохраним наш POST-запрос в какой-нибудь папке:

Сохраним запрос в отдельной папке
Сохраним запрос в отдельной папке

затем изменим тело запроса следующим образом:

Использование переменных окружения
Использование переменных окружения

в двойных фигурных скобках мы используем т.н. переменные окружения.

Далее перейдем на вкладку "Pre-request Script" и добавим следующий JS код:

Код для генерации тела запроса с рандомными данными
Код для генерации тела запроса с рандомными данными

Данная вкладка предназначена для того, чтобы добавить 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}}");

обязательно сохраняем все изменения:

Сохраняем изменения
Сохраняем изменения

кликаем правой кнопкой мыши по папке с запросом и выбираем пункт "Run collection":

В открывшемся окне у нас имеется единственный POST-запрос, который мы добавили в папку. Т.к. у нас в БД уже есть 2 записи, зададим 98 итераций и поставим галочку напротив "Save responces" чтобы сохранять ответы (мало ли, вдруг что-то пойдет не так):

Параметры для запуска коллекции
Параметры для запуска коллекции

Нажимаем "Run grid-component-backend"

Если все пройдет успешно, — Postman выполнит 98 запросов по созданию записей:

Результат выполнения 98 запросов
Результат выполнения 98 запросов

Проверить наличие записей в БД можно тремя способами: напрямую выполнить SQL-запрос в БД, выполнить GET-запрос в браузере как в предыдущем разделе, либо создать в постмэне GET-запрос, который должен вернуть нам JSON-массив с добавленными документами.

Получение cURL из браузера

Покажу как можно получить готовый cURL из вкладки Network браузера для последующего импорта в Postman.

  1. Запустим наш браузер (у меня Mozilla, но то же самое прокатит и для Google Chrome) нажмем клавишу F12 чтобы открыть консоль. Введем как и ранее в адресную строку http://127.0.0.1:3000/documents для выполнения GET-запроса и нажмем Enter. На вкладке Network должен появиться наш запрос:

  2. Если кликнуть по нему правой кнопкой — в контекстном меню можно найти замечательную опцию 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 приложения

а теперь фронт (в отдельном терминале):

Запуск frontend приложения
Запуск frontend приложения

Если фронт вы делали по инструкции из первой части, то после запуска должен запуститься браузер, который отобразит грид и попытается загрузить данные. Если успеть нажать F12 и открыть консоль, то можно увидеть вот такое сообщение:

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)

но браузеры придерживаются политики одного источника — Same-origin policy (а у нас они разные, т.к. разные порты), поэтому, если не предпринять дополнительные меры, то браузер не позволит выполнить данный запрос на бэкенд.

В NestJS есть отдельная страница по CORS, на которой описано как можно доработать приложение, чтобы браузер не ругался. Есть несколько способов, — я воспользуюсь вот таким (в файл main.ts добавим следующую опцию):

Доработка приложения для выполнения cross origin requests
Доработка приложения для выполнения cross origin requests

Сделайте такую же доработку, запустите проект с помощью команды npm run start:dev и обновите страницу в браузере:

Данные загружены
Данные загружены

Отлично! Данные загружены, все работает!

P.S> Мозилла почему-то отображает элементы для пагинации слева (а по задумке должны быть справа) и галочки выровнены по центру, хотя предусматривалось, что они будут слева. Хром же отображает все как и я хотел (как в песочнице). Если есть мысли по данному поведению — оставляйте в комментариях.

Заключение

Ну вот, наконец-то...я это сделал! Однако, чтобы Вы понимали, — все что описано в этих двух статьях — это даже не надводная часть, а лишь снежная шапка, покрывающая вершину айсберга под названием Web-разработка. Наверное, где-то вот здесь:

Красивый айсберг
Красивый айсберг

Тем не менее, очень надеюсь, что материал этих двух статей был хоть сколько-нибудь полезным и послужит отправной точкой для изучения Web-технологий.

Источник: https://habr.com/ru/post/678682/


Интересные статьи

Интересные статьи

Почти 3 года назад вышла статья Онлайн-конструктор переговорной комнаты — подбор оптимального решения ВКС, в которой рассказывается о решении задачи выбора оборудования для переговорных комнат использ...
IU (Intelligent University) - это образовательный проект, в котором я сейчас участвую. Всегда чувствовал, что образование - это та сфера, в которой я могу быть наиболее полезен обществу. И в этой стат...
Не можете разобраться, что происходит в сетевой инфраструктуре? Создание Access Control List (ACL) давно стало рутиной, но по-прежнему отнимает очень много времени? Приходится управлять несколькими ме...
Используем алгоритм. Он ключ к решению головоломки с названием "Память". Читать дальше →
Предисловие За всё время работы программистом, я сталкивался с различными задачами по доработке сайтов. И отметил одно: функций много, по разных файлам разбросанно, общей картины пространства им...