Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Модуль 2 «Введение в Nest» #2

Merged
merged 27 commits into from
Apr 3, 2024
Merged

Модуль 2 «Введение в Nest» #2

merged 27 commits into from
Apr 3, 2024

Conversation

AntonovIgor
Copy link
Contributor

No description provided.

Микросервисы в проекте размещаются в одном репозитории — монорепозитории. Вполне вероятно, при разработке микросервисов участки кода могут повторяться. Например: вспомогательные функции, интерфейсы и так далее.

Монорепозитории как правило предоставляют отдельные пространства для размещения такого кода. В `nx` для этого применяются библиотеки. Все библиотеки располагаются в директории `libs`.

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

Перенос общего кода в библиотеки — лишь одна сторона медали. О библиотека NX правильней думать как о месте, где также может быть реализовано логика приложения. А сами приложения (apps) становятся контейнерами, которые используют библиотеки. К этом вопросу мы вернёмся позже.

Для создания библиотеки применяется команда `generate`. В этом коммите мы создадим первую общую библиотеку `libs/shared/core`. В ней разместим общие интерфейсы и типы. Команда для создания библиотеки выглядит так:

```
nx generate @nx/node:library <ИмяБиблиотеки>
```

Обратите внимание на аргументы команды `generate`. В первом
аргументе передаётся пресет (подготовленные правила). На его основе
создаётся новая библиотека.

Для создания первой библиотеки воспользуемся пресетом `@nx/node`. В качестве пресета можно выбрать и для Nest. В результате выполнения команды будет создана общая библиотека.
Для общей библиотеки автоматически создаются заготовки. При
использовании пресета для Node, такой заготовкой станет: модуль с
функцией, имя которой совпадает с названием библиотеки
(`shared-core.ts`), а так же заготовка для модуля с
тестами (`shared-core.spec.ts`).

Толку от этих файлов нет и они нам не потребуются. Поэтому
удалим их и не забудем убрать ре-экспорт из модуля `index.ts`.
…`Entity`.

В этом разделе мы частично разработаем сервис `account`. Нам потребуется интерфейс для определения сущности «Пользователь». Создадим его в общей библиотеке `core`.

Файл с интерфейсом (`user.interface.ts`) разместим в директории
`core/types/src/lib`. Здесь же создадим первое перечисление
`UserRole`. В этом перечислении отразим возможные роли пользователей.

Техническое задание определяет две роли: «Администраторы»
и «Пользователи».

Обратите внимание на именование интерфейса `User`. В имени интерфейса не используется постфикс `Interface` или буквы `I`. Это ещё один из вариантов задания имени для интерфейсов. Для имени интерфейса используются короткие имена, а для классов, которые реализуют его, более ёмкие.

Например, интерфейс для определения пользователя называется «User», а класс, который его реализует, может называться `BlogUser`. В имени класса учитывается предметная область — блог.

В своих проектах вы можете выбрать любой из вариантов именования,
главное чтобы он использовался во всём проекте.

Также добавим отдельный интерфейс `AuthUser`. Он наследуется от `User` и содержит дополнительное поле `passwordHash`. В нём будем хранить хешированную версию параля.

Последним шагом, отредактируем файл `index.ts` и сделаем реэкспорт
созданных элементов.

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

Псевдонимы для путей формируются при создании общих библиотек.
Посмотреть список созданных псевдонимов вы можете в конфигурационном файле `tsconfig.base.json`, секция `paths`.

Для первой библиотеки создан псевдоним `@project/shared/core`. Если
в каком-то из сервисов потребуется импортировать интерфейс (например, `User`), то это нужно делать, опираясь на псевдоним:

```
import {User} from '@project/shared/core';
В проекте потребуются паттерны «DTO» (Data Transfer Object) и
«RDO» (Response Data Object). Для определения DTO/RDO воспользуемся классами. Чтобы упростить создание экземпляров и заполнение таких классов, установим в проект пакет `class-transformer`.
Следующим шагом подготовим ещё одну библиотеку — `shared/helpers`. В ней будем размещать вспомогательные функции, интерфейсы, которые не относятся к предметной области и так далее.

Сразу после формирования библиотеки, не забудем удалить стартовые файлы. Для создания библиотеки воспользуемся командой:

```
npx nx generate @nx/node:library --directory libs/shared
```
Потренируемся работать с общей библиотекой `shared/helpers`. Создадим новый
модуль `common.ts` и опишем в нём первую функцию — `fillDto`. Эта функция упростит использование `plainToInstance` из пакета `class-transformer`. Аргументами она принимает любую DTO/RDO
(в виде класса) и объект с данными.

После создания модуля `common.ts`, не забудем обновить `index.ts` и добавить реэкспорт.
При создании нового проекта также формируются стартовые файлы с шаблонным кодом. Набор файлов и код зависит от пресета на основании которого генерировался проект.

Например, для сервиса `users` использовался шаблон для Nest. Это привело к созданию заготовок в виде `app.module.ts`, `app.controller.ts`, `app.controller.spec.ts`, `app.service.spec.ts` и `app.service.ts`.

Такой набор неплохо подходит для совсем маленьких проектов, построенных на Nest. Обычно функциональность проекта принято разделять на отдельные модули.

Например, для сервиса `account` на данном этапе можно выделить как минимум два модуля: `auth` (аутентификация) и `blog-user` (работа с пользователями).

Дополнительные модули мы создадим позже, а пока удалим всё лишнее.
Быстро накидать структуру приложения поможет команда `generator` и
соответствующий пресет. Например, для создания новых модулей `auth` и `blog-user` выполним две команды:

```
nx generate @nx/nest:module app/blog-user --project=account
nx generate @nx/nest:module app/authentication --project=account
```

Обратите внимание, будут созданы одноимённые директории `authentication` и `blog-user`, а в них заготовки для модулей.

В корневой модуль приложения будут автоматически внесены изменения: будут подключены модули `AuthenticationModule` и `BlogUserModule`.

Другой путь — использовать библиотеки для отдельных фичей. В демо-проекте мы привели именно этот вариант. Создали две новых библиотеки: `authentication` и `blog-user`.
Генерировать можно не только модули, но и заготовки для других базовых
абстракций Nest. Например, чтобы сгенерировать контроллер для модуля `AuthenticationModule` воспользуемся командой:

```
nx generate @nx/nest:controller app/authentication --project=users
```

После выполнения команды, в директории модуль `authentication` будет создан
новый файл `authentication.controller.ts` (заготовка для контроллера) и файл
`authentication.controller.spec.ts` (заготовка для тестов). Файл с тестами можно сразу удалить, пока они не потребуются.

Аналогичным образом происходит подготовка заготовок для сервисов.

NX позволяет описывать произвольные генераторы с учётом особенностей конкретного проекта. Про эту возможность можно почитать в документации.

P.S. Обратите внимание, мы используем библиотеки для описания модулей. То есть не генерируем в `--project`.
Интерфейс предусматривает метод `toPOJO`, который будет возвращать простой объект для Entity.

Интерфейс `EntityFactory` пригодится для описания фабрик — классов, которые отвечают за создание экземпляров классов.
@AntonovIgor AntonovIgor self-assigned this Mar 27, 2024
Имплементируем подготовленные интерфейсы.
Для доступа к данным воспользуемся паттерном «Репозиторий». Многим сущностям потребуется стандартный набор операций для сохранения и извлечения данных. Поэтому подготовим максимально абстрактный интерфейс.

Мы подготовили сущность `BlogUserEntity` и теперь можем позаботиться
о сохранении пользователей в хранилище. В будущем, информацию о
пользователях будем хранить в базе данных. Но пока базы нет, мы
воспользуемся временным решением: сохраним информацию о
пользователях в памяти и заодно потренируемся в применении паттерна
«Репозиторий».

Реализуем абстрактный класс `BaseMemoryRepository`, который сможет обеспечить сохранение любой `Entity` в памяти.
Теперь создадим `BlogUserRepository`, который станет потомком `BaseMemoryRepository`. Никакие методы переопределять не будем.
…тве провайдера.

Мы закончили основные работы с модулем `BlogUserModule`. Остаётся дело
за малым: зарегистрировать созданный репозиторий в качестве
провайдера и экспортировать его из модуля.

Провайдеры могут внедряться в качестве зависимостей. Причём все детали
и нюансы Nest возьмёт на себя. Нам главное пометить класс, который
является провайдером, декоратором `@Injectable` и зарегистрировать его
в качестве провайдера.

Аннотируем декоратором `@Injectable` класс `BlogUserMemoryRepository`,
а затем в файле модуля (`blog-user.module.ts`) зарегистрируем в
качестве провайдера (секция `providers`) и сделаем провайдер
экспортируемым (секция `exports`).
Теперь посмотрим как работает внедрение зависимостей в Nest.
Репозиторий `BlogUserRepository` потребуется внутри сервиса
`AuthenticationService`.

Для внедрения его в качестве зависимости, достаточно определить в
конструкторе параметр соответствующего типа. Остальное Nest сделать
автоматически.
Нам потребуется работать с датами. Для преобразования дат из строк,
воспользуемся пакетом `dayjs`. Установим его.
Всё необходимую информацию для регистрации нового пользователя, а
также авторизации пользователя, методы сервиса будут принимать через DTO.

Создадим директорию `dto` в модуле `AuthenticationModule`, а в ней два файла:
`create-user.dto.ts` (регистрация пользователя) и `login-user.dto.ts`
Решим задачу хеширования пароля. Для этого воспользуемся внешним
пакетом — `bcrypt`. Установим пакет в основные зависимости, а также
типы для него: `@types/bcrypt`. Типы устанавливаем в качестве
dev-зависимости.
Пароли пользователей не должны хранится в открытом виде. Поэтому при
регистрации пользователя, необходимо получить хеш пароля.
Библиотека `bcrypt` в этом поможет нам.

Реализуем в классе `BlogUserEntity` метод для выполнения хеширования.
Назовём его `setPassword`.

Внутри метода получим соль с помощью функции `genSalt` из пакета
`bcrypt`. Аргументом ей необходимо передать количество раундов.
Зафиксируем это значение в одноимённой константе.

Затем запишем в свойство `passwordHash` хеш пароля, полученный с
помощью функции `hash`. Для получения хеша необходимо передать пароль в открытом виде и соль.
Следующим шагом решим задачу проверки пароля пользователя. Когда
пользователь захочет авторизоваться, он передаст пароль в открытом виде. Полученный пароль нужно сравнить с хешем, который хранится в базе данных.

Для решения этой задачи опишем метод `comparePassword`. Аргументом он принимает пароль в открытом виде. Внутри метода происходит сравнение с помощью функцию `compare` из пакета `bcrypt`. Результатом станет булево значение: `true` — пароль совпадает с хешем, `false` — нет.
Для регистрации пользователей определим новый метод в сервисе
`AuthenticationService` и напишем в нём всё необходимую логику.

Метод `register` принимает аргументом `dto` типа `CreateUserDto`.
Внутри метода извлекаем все необходимые поля, а затем начинаем
взаимодействовать с репозиторием.

Перед добавлением информации о новом пользователе, выполним проверку.
В базе не может быть двух пользователей с одинаковым email.
Аналогичным образом добавим в сервис `AuthService` методы `verifyUser`
и `getUser`. В первом выполним проверку логина и пароля
пользователя. Для этого соберём сущность пользователя и воспользуемся
методом `comparePassword`.

В методе `getUser` реализуем получение пользователя по уникальному
идентификатору.
Перед имплементацией контролера добавим несколько RDO. Они
пригодятся при отправке данных клиенту после выполнения одной из
операций.

Подготовим два RDO: `UserRdo` (используем при регистрации пользователя)
и `LoggedUserRdo` (для авторизации).

В RDO воспользуемся пакетом `class-transformer` и расставим
декораторы `@Expose`.
Контроллер в Nest — это класс, аннотированный декоратором `@Controller`
из `@nestjs/common`.

Аргументом в декоратор принимает префикс пути корневого маршрута, от
которого строятся маршруты отдельных обработчиков в контроллере.
В качестве префикса мы указываем `auth`. То есть обработчики отдельных
маршрутов (в контроллере) будут строится от этого пути.
Например: `/auth/register` (обработчик для маршрута `register`) и
так далее.

Внутри контроллера нам потребуется взаимодействовать с сервисом
`AuthenticationService`. В контексте Nest, сервис является провайдером: мы
пометили его декоратором `@Injectable` и зарегистрировали в качестве
провайдера в модуле `AuthenticationModule`.

Чтобы воспользоваться `AuthenticationService`, внедрим его в качестве зависимости
в конструктор контроллера. Для этого опишем конструктор с параметром
типа `AuthenticationService`. Остальное сделает Nest.
Добавим обработчик для маршрута `/auth/register`. В нём мы реализуем
сценарий регистрации (создания) нового пользователя.

Этот обработчик ожидает запрос, отправленный методом POST. Чтобы
закрепить за обработчиком нужный метод, мы используем декоратор
`@Post`. Аргументом передаём маршрут (`/register`).

Данные для регистрации нового пользователя клиент передаёт в теле
запроса. Для их извлечения применяется другой декоратор — `@Body`. Им
необходимо воспользоваться при определении параметра для обработчика.
В нашем примере, данные из тела запроса будут помещены в параметр `dto`.

Затем происходит обращение к сервису `AuthenticationService`. Вызывается
одноимённый метод для регистрации пользователя, а затем возвращается
результат в виде заполненного response object. Для заполнения
используем функцию `fillDto` из общей библиотеки `@project/shared/helpers`
Аналогичным образом добавим обработчики для маршрутов `/login`,
`/:id`. Внутри обработчиков происходит обращение к соответствующим
методом сервиса `AuthenticationService`.
В директории модуля «Authentication» создадим файл `authentication.http` и создадим
заготовки для проверки обработки запросов.
@AntonovIgor AntonovIgor merged commit 0697654 into main Apr 3, 2024
1 check passed
Copy link

@artytrik artytrik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Команды для nx в описании коммитов не соответствуют результату (и флаг project отмечен как deprecated).
Думаю стоит по описаниям тоже пройтись и обновить в соответствии с текущей кодовой базой.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants